Skip to content

Add a Redis state provider, the last item on the roadmap - #39

Merged
ivanvyd merged 1 commit into
mainfrom
feature/redis-state-provider
Jul 29, 2026
Merged

Add a Redis state provider, the last item on the roadmap#39
ivanvyd merged 1 commit into
mainfrom
feature/redis-state-provider

Conversation

@ivanvyd

@ivanvyd ivanvyd commented Jul 29, 2026

Copy link
Copy Markdown
Owner

The last item on the README roadmap: "A Redis state provider — the fastest option for state written on every tick."

State is written on every tick of every checker. A relational provider does a round trip to a disk-backed engine for each of those; this does one to memory. That is the whole reason to pick it.

Optimistic concurrency, in one command

SupportsOptimisticConcurrency is true, and it earns it. One hash per checker holds the state, the type it was written as, and a version — a hash rather than a plain string so the compare and the write can be a single Lua script, which Redis runs to completion without interleaving anything else:

if redis.call('HGET', KEYS[1], 'version') ~= ARGV[3] then
    return 0
end
redis.call('HSET', KEYS[1], 'value', ARGV[1], 'state_type', ARGV[2], 'version', ARGV[4])
return 1

WATCH/MULTI/EXEC would also work, but it needs its own retry loop around the optimistic failure and it holds state on the connection. A script needs neither. Creating is the same shape against EXISTS, so two writers that both find nothing cannot both create.

Tested against a real Redis, not a fake

The guarantees here are Redis's own, so a fake would only restate what the provider already believes. The tests run against redis:7-alpine in a container via Testcontainers, and skip rather than fail where there is no container runtime — the same bargain the CosmosDB combinations strike in the E2E project.

The property that matters is asserted rather than argued: twelve writers read one version, race to write it, and exactly one wins.

mutation tests failed
conditional write ignores the version 3
create-if-absent overwrites what is there 1
the version never changes 3
the key prefix is ignored 1
a state is returned as any type asked for 1

15 Redis tests, 463 total green on both net8.0 and net10.0, zero build warnings.

Other details

  • GetStatesAsync issues every read before awaiting any. StackExchange.Redis pipelines on one connection, so the dashboard listing every checker costs one round trip rather than one per checker.
  • Keys are prefixed (healthie:state: by default) so the provider stays clear of whatever else is on that server.
  • AddHealthieRedis() with no configuration string reuses an IConnectionMultiplexer the application already registers, rather than opening a second connection to the same server.
  • A hash written before this provider versioned its writes reports as unversioned, and the caller writes it unconditionally as it did before — the same upgrade path the relational providers got.

Durability is Redis's, not this package's

Redis is in memory. Whether state survives a restart is your Redis configuration, not something this provider can promise, and the package README says so instead of implying otherwise.

The roadmap is now empty

Every item on it has shipped. What replaces it is written as what it actually is — two decisions, not two features: the Healthie.Api / dashboard authorization defaults, and restore not being hash-pinned. Both are deliberate as they stand and both are yours to make.

No release — version stays at 3.1.4.

State is written on every tick of every checker. A relational provider does a
round trip to a disk-backed engine for each of those; this does one to memory.
That is the whole reason to pick it, and it is why the roadmap asked for it.

One hash per checker, holding the state, the type it was written as, and a
version. A hash rather than a plain string so the version can be compared and
swapped in the same command as the write: TrySetStateAsync is a Lua script, and
Redis runs one to completion without interleaving anything else. WATCH/MULTI/
EXEC would also work, but it needs its own retry loop around the optimistic
failure and it holds state on the connection; a script needs neither. Creating
is the same shape against EXISTS, so two writers that both find nothing cannot
both create.

That last property is asserted against a real Redis rather than argued for:
twelve writers read one version, race to write it, and exactly one wins. A
fake would only have restated what the provider already believes, so the tests
run against redis:7-alpine in a container through Testcontainers, and skip
rather than fail where there is no container runtime -- the same bargain the
CosmosDB combinations strike in the E2E project.

GetStatesAsync issues every read before awaiting any. StackExchange.Redis
pipelines on one connection, so the dashboard listing every checker costs one
round trip rather than one per checker.

Durability is whatever the Redis is configured for -- snapshots, AOF, or a
managed offering's own promises -- and the README says that rather than
implying the provider can offer more than the server does.

The roadmap is now empty. What remains in its place is written as what it is:
two decisions, not two features.
@ivanvyd
ivanvyd merged commit 67b06b0 into main Jul 29, 2026
7 of 8 checks passed
@ivanvyd
ivanvyd deleted the feature/redis-state-provider branch July 30, 2026 02:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant